虽然前端对字段进行了校验约束,但在后端代码中,也很有必要对字段进行约束校验。防止用户直接调用 api 接口进行请求。

一、注解校验参数

1、在 controller 层,首先需要在类上添加 @Validated 注解。

2、方法入参,分为两种情况:一种是单独参数,另一种是对象参数。

  • 单独参数:对于单独参数来说,通常使用 @PathVariable 和 @RequestParam 注解修饰。可以直接在 @PathVariable 和 @RequestParam 注解前添加 @Validated @Length(min = , max = , message = “”) 注解进行参数校验。其中 @Validated 可以替换为 @Valid。
  • 对象参数:对于对象参数来说,通常使用 @RequestBody 注解修饰。分为三个步骤:
    • 在 @RequestBody 注解前添加 @Valid,注意,必须是 @Valid 注解,@Validated 注解无效。
    • 在对象实体类中的属性字段上,添加校验注解,比如 @NotEmpty、@Length 等。
    • 在校验对象参数后面紧跟 BindingResult result 参数,@Valid 会将校验的结果存储到 BindingResult 中。如果没有,代码则会报异常。虽然不加 BindingResult 参数也能实现字段校验,但代码总归不是那么优雅。
    • 以上三步缺一不可,只有这样,才能实现字段校验。

3、pom 依赖

在 spring-boot-starter-web 里面是有 hibernate-validator 这个包的,我用的 spring-boot-starter-web 版本是 spring boot 2.2.7.RELEASE。如果是用的 spring boot 1.x 的话,spring-boot-starter-web 里面包的版本比较低,不好用,所以建议添加下述依赖,将 hibernate-validator 版本调整到 6.x 就好了。

1
2
3
4
5
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>

添加了上述较高版本后,@NotEmpty 等注解引入的路径为 javax.validation.constraints.NotEmpty; 一定要确认好是这个路径,低版本的路径和这个不一致,咱们不用低版本的。因为用低版本时,做统一异常处理,校验信息有问题。

补充:还需要把低版本的jar包给排除掉:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>hibernate-validator</artifactId>
<groupId>org.hibernate</groupId>
</exclusion>
</exclusions>
</dependency>

二、字段校验常用注解

引入包为:

1
2
import javax.validation.constraints.*; 
import org.hibernate.validator.constraints.Length;
  • @NotNull :字段不能为空。
  • @NotEmpty :验证注解的元素值不为 null 且不为空(字符长度、集合大小、 map 大小、数组长度不能为零)
  • @NotBlank :验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于 @NotEmpty,@NotBlank 只应用于字符串且在比较时会去除字符串的空格。
  • @Size(max, min) :字段元素大小范围。( null 也视为有效元素)
  • @Null :字段必须为空。
  • @Min :字段最小值。(不适用 double 和 float )
  • @Max :字段最大值。(不适用 double 和 float )
  • @Range :字段值范围。( @Min 和 @Max 结合)
  • @Length :字段长度范围。
  • @Email :字段必须符合 Email 格式。
  • @Pattern :正则表达式,不能用在 Integer、Character 类型。例如:@Pattern(regexp = “^[a-zA-Z]\w+$”, message = “name命名仅支持数字,字母(大小写)和下划线组合,且必须以字母开头。”)
  • @Digits(integer,fraction) :限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
  • @Future :限制必须是一个将来的日期
  • @Past :限制必须是一个过去的日期
  • @AssertTrue :推断是否正确。
1
2
3
4
5
6
7
8
9
10
11
@Range(min = 0, max = 100, message = "数值1不在正确范围")
private Integer integer1;

@Range(min = 0, max = 100, message = "数值2不在正确范围")
private Integer integer2;

@AssertTrue(message = "integer1 必须小于 integer2")
@JsonIgnore // 表示忽略该字段,如果不加该字段,@Data注解会在swagger body对象中生产test字段。
public boolean isTestValid() {
return this.integer1 < this.integer2;
}

另外提一嘴,@AssertTrue 是真的好用,可以更加细化的判断字段值是否符合判断标准。

补充:

之后,在项目里面用 @AssertTrue 的时候,发现被其定义的方法没有执行,当时我被 @AssertTrue 修饰的方法名是:judgeShardNum() ,不生效。

经过谷歌,才知道被 @AssertTrue 修饰的方法名还有约束,必须是 get 或 is 开头的才可以。所以我就将 judgeShardNum() 重命名为了 isShardNumValid() ,这样程序校验果真就生效了。

具体可参考:

https://stackoverflow.com/questions/12935360/bean-validation-on-method/12950573#12950573

三、字段校验失败返回

建议还是通过 @ControllerAdvice 和 @ExceptionHandler 注解写一个统一异常返回类,这样,在统一异常返回类里面,直接再加一个 ValidationException 异常捕获,就可以对字段校验失败的请求进行统一返回,进而提示用户。如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 捕获异常 针对不同异常返回不同内容的固定格式信息
* 拦截所有的异常,并且返回 json 格式的信息
*
* @param e 异常
* @return result
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result<Object> handle(HttpServletResponse response, Exception e) {
log.error("Exception:", e);
if (e instanceof CustomException) {
// 如果是我们自定义的异常,就直接返回我们异常里面设置的信息
CustomException customException = (CustomException) e;
return Result.failed(customException.getCode(), customException.getMsg());
} else if (e instanceof ValidationException) {
return Result.failed(ResultEnum.VALIDATION_EXCEPTION.getCode(), e.getMessage());
} else if (e instanceof BindException) {
return Result.failed(ResultEnum.VALIDATION_EXCEPTION.getCode(), ((BindException) e).getBindingResult().getAllErrors().get(0).getDefaultMessage());
} else if (e instanceof MethodArgumentNotValidException) {
return Result.failed(ResultEnum.VALIDATION_EXCEPTION.getCode(), ((MethodArgumentNotValidException) e).getBindingResult().getAllErrors().get(0).getDefaultMessage());
} else if (e instanceof MethodArgumentTypeMismatchException) {
return Result.failed(ResultEnum.VALIDATION_EXCEPTION.getCode(), e.getMessage());
} else if (e instanceof IllegalArgumentException) {
return Result.failed(ResultEnum.ILLEGAL_ARGUMENT_EXCEPTION.getCode(), e.getMessage());
} else if (e instanceof HttpMessageNotReadableException) {
return Result.failed(ResultEnum.HTTP_MESSAGE_NOT_READABLE);
} else if (e instanceof ResourceAccessException) {
return Result.failed(ResultEnum.HTTP_HOST_CONNECT_EXCEPTION);
} else if (e instanceof HttpRequestMethodNotSupportedException) {
response.setStatus(ResultEnum.http_status_method_not_allowed.getCode());
return Result.failed(ResultEnum.http_status_method_not_allowed.getCode(), e.getMessage());
} else {
response.setStatus(ResultEnum.http_status_bad_request.getCode());
return Result.failed(ResultEnum.http_status_bad_request);
}
}

四、内部类、嵌套类字段校验

如果需要在内部类校验的话,需要先在字段上添加 @Valid ,然后再在内部类或嵌套类的字段上直接加校验注解,比如@NotEmpty,就会生效了。

  • 步骤一
1
2
@Valid
private List<Schema> schema;
  • 步骤二
1
2
3
4
5
6
import javax.validation.constraints.NotEmpty;

public static class Schema {
@NotEmpty(message = "fieldName cannot be empty.")
private String fieldName;
}